#include "PBFTest.h"
#include "Imgui/imgui.h"
#include "Common.h"
using namespace Simulation;
using namespace Utilities;

#define ParticleRendering
//#define ScreenSpaceRendering
namespace Test {
	
	bool PBFTest::doPause = true;
	bool PBFTest::doBlur = false;
	int PBFTest::substepTimes =4;
	FluidModel PBFTest::model;
	TimeStepFluidModel PBFTest::simulation;
	const unsigned int PBFTest::length = 15;
	const unsigned int PBFTest::width = 15;
	const unsigned int PBFTest::height = 20;
	const double PBFTest::particleRadius = static_cast<double>(0.025);
	const double PBFTest::containerLength = (length + 1) * particleRadius * static_cast<double>(2.0 * 3.0);
	const double PBFTest::containerWidth = (width + 1) * particleRadius * static_cast<double>(2.0);
	const double PBFTest::containerHeight = 4.0;
	
	PBFTest::PBFTest():particleModel("..\\..\\OpenGLResource\\model\\common\\SphereS\\Sphere_16.obj")
	{
		
#ifdef ParticleRendering
		particleShader = new Shader("ParticleVert.glsl", "ParticleFrag.glsl");
#endif 

#ifdef ScreenSpaceRendering
		liquidDepthShader = new Shader("liquidDepthVert.glsl", "liquidDepthFrag.glsl");
		liquidThicknessShader = new Shader("liquidThicknessVert.glsl", "liquidThicknessFrag.glsl");
		liquidShader = new Shader("liquidVert.glsl", "liquidFrag.glsl");
		GaussianBlurShader = new Shader("GaussianBlurVert.glsl", "GaussianBlurFrag.glsl");
#endif

	}
	PBFTest::~PBFTest()
	{

	}
	void PBFTest::Enter()
	{
		buildModel();
		ParticleData& pd = model.getParticles();
		const unsigned int nParticles = pd.size();
		glEnable(GL_DEPTH_TEST);
		modelMatrices = new glm::mat4[nParticles];
		for (int i = 0; i < nParticles; ++i)
		{
			glm::mat4 model;
			glm::dvec3 sourcedata = pd.getPosition(i);
			glm::vec3 targetdata = glm::vec3(sourcedata);
			model = glm::translate(model, targetdata);
			model = glm::scale(model, glm::vec3(0.0001f,0.0001f,0.0001f));
			modelMatrices[i] = model;
		}

		glGenBuffers(1, &modelMatrixVBO);
		glBindBuffer(GL_ARRAY_BUFFER, modelMatrixVBO);
		glBufferData(GL_ARRAY_BUFFER, nParticles * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);
	
		for (int i = 0; i < particleModel.meshes.size(); i++)
		{
			unsigned int VAO = particleModel.meshes[i].GetVAO();
			glBindVertexArray(VAO);
			GLsizei vec4Size = sizeof(glm::vec4);
			glEnableVertexAttribArray(3);
			glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)0);
			glEnableVertexAttribArray(4);
			glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(vec4Size));
			glEnableVertexAttribArray(5);
			glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(2 * vec4Size));
			glEnableVertexAttribArray(6);
			glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(3 * vec4Size));

			glVertexAttribDivisor(3, 1);
			glVertexAttribDivisor(4, 1);
			glVertexAttribDivisor(5, 1);
			glVertexAttribDivisor(6, 1);

			glBindVertexArray(0);
		}
#ifdef ScreenSpaceRendering
		#pragma region SceenSpaceFluidTexture
		//depth framebuffer
		glGenFramebuffers(1, &depthFrameBuffer);
		glBindFramebuffer(GL_FRAMEBUFFER, depthFrameBuffer);

		glGenTextures(1, &depthTextureBuffer);
		glBindTexture(GL_TEXTURE_2D, depthTextureBuffer);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, ScreenWidth, ScreenHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
		GLfloat borderColor[] = { 1.0, 1.0, 1.0, 1.0 };
		glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthTextureBuffer, 0);

		if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
			std::cout << "Depth Framebuffer not complete!" << std::endl;
		else
			std::cout << "Depth Framebuffer complete!" << std::endl;
		glBindFramebuffer(GL_FRAMEBUFFER, 0);

		//depth blur framebuffer
		glGenFramebuffers(1, &depthBlurFrameBuffer);
		glBindFramebuffer(GL_FRAMEBUFFER, depthBlurFrameBuffer);

		glGenTextures(1, &depthBlurTextureBuffer);
		glBindTexture(GL_TEXTURE_2D, depthBlurTextureBuffer);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, ScreenWidth, ScreenHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
		//GLfloat borderColor[] = { 1.0, 1.0, 1.0, 1.0 };
		glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthBlurTextureBuffer, 0);

		if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
			std::cout << "Depth Blur Framebuffer not complete!" << std::endl;
		else
			std::cout << "Depth Blur Framebuffer complete!" << std::endl;
		glBindFramebuffer(GL_FRAMEBUFFER, 0);


		//thickness framebuffer
		glGenFramebuffers(1, &thicknessFrameBuffer);
		glBindFramebuffer(GL_FRAMEBUFFER, thicknessFrameBuffer);

		glGenTextures(1, &thicknessTextureBuffer);
		glBindTexture(GL_TEXTURE_2D, thicknessTextureBuffer);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, ScreenWidth, ScreenHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, thicknessTextureBuffer, 0);

		if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
			std::cout << "Thickness Framebuffer not complete!" << std::endl;
		else
			std::cout << "Thickness Framebuffer complete!" << std::endl;
		glBindFramebuffer(GL_FRAMEBUFFER, 0);



		//Screen 

		glGenVertexArrays(1, &screenVAO);
		glGenBuffers(1, &screenVBO);
		glGenBuffers(1, &screenEBO);

		glBindVertexArray(screenVAO);
		glBindBuffer(GL_ARRAY_BUFFER, screenVBO);
		glBufferData(GL_ARRAY_BUFFER, screenVertices.size() * sizeof(float),
			&screenVertices[0], GL_STATIC_DRAW);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, screenEBO);
		glBufferData(GL_ELEMENT_ARRAY_BUFFER, screenIndices.size() * sizeof(unsigned int),
			&screenIndices[0], GL_STATIC_DRAW);
		// vertex Positions
		glEnableVertexAttribArray(0);
		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 8,
			static_cast<void*>(0));
		// vertex normals
		glEnableVertexAttribArray(1);
		glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 8,
			(void*)(3 * sizeof(float)));
		// vertex texture coords
		glEnableVertexAttribArray(2);
		glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 8,
			(void*)(6 * sizeof(float)));

		glBindVertexArray(0);

		backgroundTexture = LoadImageToGPU("background.jpg", GL_RGB, GL_RGB, 2);
#pragma endregion
#endif // ScreenSpaceRendering



		
	}
	void PBFTest::Leave()
	{
		reset();

		delete[] modelMatrices;
#ifdef ParticleRendering
		delete particleShader;
#endif // ParticleRendering

		
#ifdef ScreenSpaceRendering
		#pragma region ScreenSpaceFluidField
		glDeleteFramebuffers(1, &depthFrameBuffer);
		glDeleteFramebuffers(1, &thicknessFrameBuffer);
		glDeleteFramebuffers(1, &depthBlurFrameBuffer);
		glDeleteTextures(1, &depthBlurTextureBuffer);
		glDeleteTextures(1, &depthTextureBuffer);
		glDeleteTextures(1, &thicknessTextureBuffer);
		glDeleteTextures(1, &backgroundTexture);

		delete liquidDepthShader;
		delete liquidThicknessShader;
		delete liquidShader;
		delete GaussianBlurShader;
#pragma endregion
#endif // ScreenSpaceRendering

	}
	void PBFTest::OnRender()
	{
		glm::dmat4 view;
		glm::dmat4 projection;
		
		view = camera.GetViewMatrix();
		projection = glm::perspective(glm::radians(camera.Zoom), ScreenWidth / ScreenHeight, 0.1f, 1000000000.0f);

		ParticleData& pd = model.getParticles();
		const unsigned int nParticles = pd.size();
		//START_TIMING("ForParticles");
		#pragma omp parallel for schedule(static)  
		for (int i = 0; i < nParticles; ++i)
		{
			glm::mat4 model;
			model = glm::translate(model, glm::vec3(pd.getPosition(i)));
			model = glm::scale(model, glm::vec3(0.04f, 0.04f, 0.04f));
			modelMatrices[i] = model;
		}
		//STOP_TIMING_PRINT

		//START_TIMING("ParticleBuffer");
		glBindBuffer(GL_ARRAY_BUFFER, modelMatrixVBO);
		glBufferData(GL_ARRAY_BUFFER, nParticles * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);

#ifdef ParticleRendering
		#pragma region PaticleFluid
		glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		particleShader->use();
		particleShader->setMat4("view", view);
		particleShader->setMat4("projection", projection);
		//STOP_TIMING_PRINT

#pragma region PaticleRender
//START_TIMING("DrawCall");
		for (int i = 0; i < particleModel.meshes.size(); i++)
		{
			glBindVertexArray(particleModel.meshes[i].GetVAO());
			glDrawElementsInstanced(
				GL_TRIANGLES, particleModel.meshes[i].indices.size(), GL_UNSIGNED_INT, 0, nParticles
			);
		}
		//STOP_TIMING_PRINT
#pragma endregion
#pragma endregion
#endif // ParticleRendering	

#ifdef ScreenSpaceRendering
		#pragma region SceenSpaceFluid
		float aspect = ScreenWidth / ScreenHeight;
		float fovy = camera.Zoom;
		float pointScale = 1.0f * ScreenWidth / aspect * (1.0f / tanf(glm::radians(fovy) * 0.5f));
		// RT Water Render
#pragma region DepthTexture
		glBindFramebuffer(GL_FRAMEBUFFER, depthFrameBuffer);
		glEnable(GL_DEPTH_TEST);
		glDisable(GL_BLEND);
		glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
		glEnable(GL_PROGRAM_POINT_SIZE);
		glEnable(GL_VERTEX_PROGRAM_POINT_SIZE);
		float particleRadius = model.getParticleRadius();
		liquidDepthShader->use();
		liquidDepthShader->setFloat("pointScale", pointScale);
		liquidDepthShader->setFloat("pointSize", particleRadius);
		liquidDepthShader->setMat4("view", view);
		liquidDepthShader->setMat4("projection", projection);
		for (int i = 0; i < particleModel.meshes.size(); i++)
		{
			glBindVertexArray(particleModel.meshes[i].GetVAO());
			glDrawElementsInstanced(GL_TRIANGLES, particleModel.meshes[i].indices.size(), GL_UNSIGNED_INT, 0, nParticles);
		}
		glBindVertexArray(0);

		glDisable(GL_PROGRAM_POINT_SIZE);
		glBindFramebuffer(GL_FRAMEBUFFER, 0);
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, 0);
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, 0);
		glActiveTexture(GL_TEXTURE2);
		glBindTexture(GL_TEXTURE_2D, 0);
		if (doBlur)
			Blur::DepthGaussianBlur(depthTextureBuffer, depthBlurFrameBuffer, GaussianBlurShader, ScreenWidth, ScreenHeight, screenVAO);
#pragma endregion 

#pragma region ThicknessTexture
		// render state.
		glBindFramebuffer(GL_FRAMEBUFFER, thicknessFrameBuffer);
		glEnable(GL_BLEND);
		glBlendFunc(GL_ONE, GL_ONE);
		glDepthMask(GL_FALSE);
		glEnable(GL_DEPTH_TEST);
		glDisable(GL_DEPTH_TEST);
		glEnable(GL_PROGRAM_POINT_SIZE);
		glEnable(GL_VERTEX_PROGRAM_POINT_SIZE);
		glClearColor(0.0, 0.0, 0.0, 1.0);
		glClear(GL_COLOR_BUFFER_BIT);
		liquidThicknessShader->use();
		liquidThicknessShader->setFloat("pointScale", pointScale);
		liquidThicknessShader->setFloat("pointSize", particleRadius);
		liquidThicknessShader->setMat4("view", view);
		liquidThicknessShader->setMat4("projection", projection);
		for (int i = 0; i < particleModel.meshes.size(); i++)
		{
			glBindVertexArray(particleModel.meshes[i].GetVAO());
			glDrawElementsInstanced(GL_TRIANGLES, particleModel.meshes[i].indices.size(), GL_UNSIGNED_INT, 0, nParticles);
		}
		glBindVertexArray(0);
		glDepthMask(GL_TRUE);
		glDisable(GL_BLEND);
		glDisable(GL_PROGRAM_POINT_SIZE);
		glBindFramebuffer(GL_FRAMEBUFFER, 0);
#pragma endregion

#pragma region Surface
		glDisable(GL_DEPTH_TEST);
		glDisable(GL_BLEND);

		liquidShader->use();

		liquidShader->setVec3("cameraPos", camera.Position);
		liquidShader->setVec3("dirLight.direction", glm::normalize(m_direction));

		liquidShader->setVec3("dirLight.ambient", m_ambient);
		liquidShader->setVec3("dirLight.diffuse", m_diffuse);
		liquidShader->setVec3("dirLight.specular", m_specular);
		liquidShader->setInt("depthTex", 0);
		liquidShader->setInt("thicknessTex", 1);
		liquidShader->setInt("backgroundTex", 2);
		//liquidShader->setInt("backgroundDepthTex", 3);
		glm::vec4 m_liquidColor = glm::vec4(.275f, 0.65f, 0.85f, 0.5f);
		liquidShader->setVec4("liquidColor", m_liquidColor);
		liquidShader->setMat4("view", view);
		liquidShader->setMat4("projection", projection);
		liquidShader->setMat4("invViewMatrix", glm::inverse(view));
		liquidShader->setMat4("invProjectMatrix", glm::inverse(projection));

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, depthTextureBuffer);
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, thicknessTextureBuffer);
		glActiveTexture(GL_TEXTURE2);
		glBindTexture(GL_TEXTURE_2D, backgroundTexture);

		glBindVertexArray(screenVAO);
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, 0);
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, 0);
		glActiveTexture(GL_TEXTURE2);
		glBindTexture(GL_TEXTURE_2D, 0);
#pragma endregion

#pragma endregion
#endif // ScreenSpaceRendering



	}
	void PBFTest::OnImguiRender()
	{
		
		ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
		ImGui::Checkbox("Pause", &doPause);
		ImGui::Checkbox("Blur", &doBlur);
		if (ImGui::Button("Reset"))
		{
			reset();
		}
		
	}
	void PBFTest::Update(float deltaTime)
	{
		
		//LOG_INFO << deltaTime;
		timeStep();
		
	}
	//-------------------------------------------------------------------------------------//
	void PBFTest::buildModel()
	{
		//set timestep
		TimeManager::getCurrent()->setTimeStepSize(static_cast<double>(0.0025));
		createBreakingDam();
		// 
	}
	//do a simulation timestep depends on timestep size
	void PBFTest::timeStep()
	{
		if (doPause)
			return;

		// Simulation code
		for (unsigned int i = 0; i < substepTimes; i++)
			simulation.step(model);
	}
	/* Create a breaking dam scenario*/
	void PBFTest::createBreakingDam()
	{
		LOG_INFO << "Initialize fluid particles";
		const double diam = 2.0 * particleRadius;
		const double startX = -static_cast<double>(0.5) * containerLength + diam;
		const double startY = diam;
		const double startZ = -static_cast<double>(0.5) * containerWidth + diam;
		const double yshift = sqrt(static_cast<double>(3.0)) * particleRadius;

		std::vector<glm::dvec3> fluidParticles;//One dimention
		fluidParticles.resize(length * height * width);

		#pragma omp parallel default(shared)
		{
			#pragma omp for schedule(static)  
			for (int i = 0; i < (int)length; i++)
			{
				for (unsigned int j = 0; j < height; j++)
				{
					for (unsigned int k = 0; k < width; k++)
					{
						fluidParticles[i * height * width + j * width + k] = diam * 
							glm::dvec3((double)i, (double)j, (double)k) + glm::dvec3(startX, startY, startZ);
					}
				}
			}
		}
		model.setParticleRadius(particleRadius);
		std::vector<glm::dvec3> boundaryParticles;
		initBoundaryData(boundaryParticles);

		model.initModel((unsigned int)fluidParticles.size(), 
			fluidParticles.data(), (unsigned int)boundaryParticles.size(),
			boundaryParticles.data());

		LOG_INFO << "Number of particles: " << length * height * width;
		LOG_INFO << "Number of boundryParticles: " << boundaryParticles.size();
	}
	void PBFTest::addWall(const glm::dvec3& minX, const glm::dvec3& maxX, std::vector<glm::dvec3>& boundaryParticles)
	{
		const double particleDistance = static_cast<double>(2.0) * model.getParticleRadius();

		const glm::dvec3 diff = maxX - minX;
		const unsigned int stepsX = (unsigned int)(diff[0] / particleDistance) + 1u;
		const unsigned int stepsY = (unsigned int)(diff[1] / particleDistance) + 1u;
		const unsigned int stepsZ = (unsigned int)(diff[2] / particleDistance) + 1u;

		const unsigned int startIndex = (unsigned int)boundaryParticles.size();
		boundaryParticles.resize(startIndex + stepsX * stepsY * stepsZ);

		#pragma omp parallel default(shared)
		{
			#pragma omp for schedule(static)  
			for (int j = 0; j < (int)stepsX; j++)
			{
				for (unsigned int k = 0; k < stepsY; k++)
				{
					for (unsigned int l = 0; l < stepsZ; l++)
					{
						const glm::dvec3 currPos = minX + glm::dvec3(j * particleDistance, 
							k * particleDistance, 
							l * particleDistance);
						boundaryParticles[startIndex + j * stepsY * stepsZ + k * stepsZ + l] = currPos;
					}
				}
			}
		}
	}
	void PBFTest::initBoundaryData(std::vector<glm::dvec3>& boundaryParticles)
	{
		const double x1 = -containerLength / 2.0;
		const double x2 = containerLength / 2.0;
		const double y1 = 0.0; 
		const double y2 = containerHeight;
		const double z1 = -containerWidth / 2.0;
		const double z2 = containerWidth / 2.0;
		
		const double diam = 2.0 * particleRadius;

		// Floor
		addWall(glm::dvec3(x1, y1, z1), glm::dvec3(x2, y1, z2), boundaryParticles);
		// Top
		addWall(glm::dvec3(x1, y2, z1), glm::dvec3(x2, y2, z2), boundaryParticles);
		// Left
		addWall(glm::dvec3(x1, y1, z1), glm::dvec3(x1, y2, z2), boundaryParticles);
		// Right
		addWall(glm::dvec3(x2, y1, z1), glm::dvec3(x2, y2, z2), boundaryParticles);
		// Back
		addWall(glm::dvec3(x1, y1, z1), glm::dvec3(x2, y2, z1), boundaryParticles);
		// Front
		addWall(glm::dvec3(x1, y1, z2), glm::dvec3(x2, y2, z2), boundaryParticles);
	}

	void PBFTest::reset()
	{
		Timing::printAverageTimes();
		Timing::printTimeSums();
		Timing::reset();

		model.reset();
		simulation.reset();
		TimeManager::getCurrent()->setTime(0.0);
	}

}